今天,我將介紹 contentChildren,它是 @ContentChild 裝飾器 (decorator) 的訊號對應部分。
ContentChildren 裝飾器傳回 ReadT[],而 contentChildren 函數傳回 `Signal<readonly ReadT[]>。ContentChildren 裝飾器 (decorator) 的 static 屬性從 contentChild 函數中刪除。在下面的例子中,我展示了 contentChildren 如何透過範本變數 (template variables)、ngTemplates 和 Angular 組件查詢元素
<app-query-by-type>
<ng-template let-now><p>Custom Header 3, now = {{ now }}</p></ng-template>
<ng-template let-now>
<p>Custom Body 3</p>
<p>now = {{ now }}</p>
</ng-template>
</app-query-by-type>
templates = contentChildren(TemplateRef);
constructor() {
console.log('constructor', this.templates());
}
ngAfterContentInit(): void {
console.log('ngAfterContentInit', this.templates());
}
contentChildren 函數在 OnInit hook執行後可用。 在 constructor 中,元素的數量為0。在ngOnInit 和 ngContentInit 中,TemplateRef 的數量為2。
在以下例子中,我展示了 viewChildren 如何透過範本變數 (template variables)、指令 (directives) 和 Angular 元素進行查詢。
import { Component, contentChild, effect, ElementRef, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-query-by-variable',
standalone: true,
imports: [FormsModule],
template: `
<div class="container">
<h3>Query ContentChildren by variables</h3>
<label for="color">
<span>Color: </span>
<select id="color" name="color" [(ngModel)]="color">
<option value="">----</option>
<option value="red">red</option>
<option value="yellow">yellow</option>
<option value="cyan">cyan</option>
<option value="goldenrod">goldenrod</option>
<option value="pink">pink</option>
</select>
</label>
<ng-content></ng-content>
</div>
`,
})
export class AppQueryByVariableComponent {
color = signal('');
}
AppQueryByVaraibleComponent 組件具有用於頁首、頁尾和正文的 <ng-content /> 元素。 它還具有一個下拉式選單來選擇元素的背景顏色。
<app-query-by-variable>
<div #divs class="projection">Custom Header 2</div>
<div #divs class="projection">Custom Body</div>
<div #divs class="projection">
<ul>
<li>Custom Footer</li>
<li>Custom Footer 2</li>
</ul>
</div>
</app-query-by-variable>
在 App 組件中,div 元素 被投影到 <app-query-by-variable> 的預設 <ng-content>。 div 元素具有相同的範本變數 #div,我們可以使用 contentChildren 函數來查詢所有 <div> 元素。
divs = contentChildren('divs', { read: ElementRef });
contentChildren 函數透過範本變數 (template variables) 查詢元素。 第二個參數 { read: ElementRef } 期望每個項目都是 ElementRef。
constructor() {
effect(() => {
this.divs().forEach((div) =>
div.nativeElement.style.backgroundColor = this.color()
);
});
}
當 color signal 更新時,effect 會執行邏輯來變更背景顏色的 CSS 樣式。 contentChildren 函數傳回一個 ElementRef array,它可以是一個 empty array。迭代 (iterate) 該 array 以更新 div 元素的背景顏色。
@Directive({
selector: '[someDirective]',
standalone: true,
})
export class AppSomeDirective {
templates = contentChildren(TemplateRef);
}
我們可以寫一個 AppSomeDirective 指令 (directive),使用 contentChildren 來查詢投影的 ngTemplates。
@Component({
selector: 'app-query-by-type',
standalone: true,
imports: [AppSomeDirective, NgTemplateOutlet, FormsModule],
template: `
<div class="container">
<h3>Query ContentChildren by TemplateRef</h3>
<div someDirective>
<ng-template>
<p>Projected item</p>
<p>Projected item 2</p>
<p>Projected Item 3</p>
</ng-template>
<ng-template>
<p>Template 2</p>
</ng-template>
</div>
@for (template of directive().templates(); track $index) {
<ng-container *ngTemplateOutlet="template"></ng-container>
}
<label for="index">
<span>Template: </span>
<select id="index" name="index" [(ngModel)]="index" style="margin-right: 0.25rem;">
<option value="">----</option>
@for (x of templates(); track $index) {
<option [value]="$index">{{ $index + 1 }}</option>
}
</select>
</label>
<button (click)="changeTemplate()">Change template</button>
<ng-container *ngTemplateOutlet="dynamicTemplate() || defaultTemplate"></ng-container>
<ng-template #defaultTemplate>
<p>Choose a template from the dropdown.</p>
</ng-template>
</div>
`,
})
export class AppQueryByDirectiveComponent {
index = signal('');
directive = viewChild.required(AppSomeDirective);
}
div 元素有一個 someDirective 屬性;因此,指令 (directive) 的 contentChildren 可以檢索兩個 ngTemplate。此組件也可以查詢投影在 <app-query-by-type> 標記內的 ngTemplates。
@for (template of directive().templates(); track $index) {
<ng-container *ngTemplateOutlet="template">
</ng-container>
}
AppQueryByDirectiveComponent 組件使用 viewChild 函數來查詢指令 (directive) 並存取其 contentChildren。該指令的 ngTemplates 被指派給 ngTemplateOutlet 指令來渲染動態內容。
<label for="index">
<span>Template: </span>
<select id="index" name="index" [(ngModel)]="index" style="margin-right: 0.25rem;">
<option value="">----</option>
@for (x of templates(); track $index) {
<option [value]="$index">{{ $index + 1 }}</option>
}
</select>
</label>
<button (click)="changeTemplate()">Change template</button>
<ng-container *ngTemplateOutlet="dynamicTemplate() || defaultTemplate"></ng-container>
<ng-template #defaultTemplate>
<p>Choose a template from the dropdown.</p>
</ng-template>
export class AppQueryByDirectiveComponent {
index = signal('');
templates = contentChildren(TemplateRef);
dynamicTemplate = signal<TemplateRef<any> | undefined>(undefined);
changeTemplate() {
const idx = this.index();
const t = idx ? this.templates()[+idx] : undefined;
this.dynamicTemplate.set(t);
}
}
AppQueryByDirectiveComponent 組件使用 contentChildren 函數來查詢預設 <ng-content> 中存在的 TemplateRef。當使用者按一下該按鈕時,將執行 changeTemplate 以確定要顯示的範本。 當所選索引為空字串時,將顯示預設範本。否則,組件將顯示 <app-query-by-type> 標記中投影的範本。
<app-query-by-type>
<ng-template><p>Custom Header 3</p></ng-template>
<ng-template><p>Custom Body 3</p></ng-template>
</app-query-by-type>
在 App 組件中,ngTemplates 被投影到 <app-query-by-type> 中,以便 contentChildren 函數可以查詢它們並將它們指派給 templates signal。
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
const imgURL = 'https://picsum.photos/300/200';
@Component({
selector: 'app-photo',
standalone: true,
template: `
<div class="photo">
<img [src]="img()" alt="Random picture" />
</div>
`,
})
export default class AppPhotoComponent {
#random = signal(Date.now());
img = computed(() => `${imgURL}?random=${this.#random()}`)
loadImage() {
this.#random.set(Date.now());
}
}
AppPhotoComponent 組件具有附加到圖像 URL 的random seed signal。 當 loadImage 函數更新signal value 時,img computed signal會產生一個新的圖片 URL。
import { Component, ChangeDetectionStrategy, contentChildren, signal } from '@angular/core';
import AppPhotoComponent from './photo.component';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-photo-wrapper',
standalone: true,
imports: [FormsModule],
template: `
<div class="photo-wrapper">
<div class="photos">
<ng-content />
</div>
<div>
<label for="index">
<span>Photo: </span>
<select id="index" name="index" [(ngModel)]="index">
<option value="">----</option>
@for (x of photos(); track $index) {
<option [value]="$index">{{ $index + 1 }}</option>
}
</select>
</label>
<button (click)="changeImage()">Change photo</button>
</div>
</div>
`,
})
export default class AppPhotoWrapperComponent {
index = signal('');
photos = contentChildren(AppPhotoComponent);
changeImage() {
const strIdx = this.index();
if (strIdx) {
const index = +this.index();
this.photos()[index].loadImage();
}
}
}
AppPhotoWrapperComponent 組件包含一個預設的 <ng-content>,我們可以用它來投影 AppPhotoComponent 組件。此組件使用 contentChidren 函數來查詢所有 AppPhotoComponent 組件。下拉清單使用 ngModel 將值綁定到 index signal。當使用者點擊該按鈕時,changeImage 方法將呼叫 loadImage 方法來顯示新圖片。
<app-photo-wrapper>
<app-photo class="photo" />
<app-photo class="photo" />
<app-photo class="photo" />
</app-photo-wrapper>
在 <app-photo-wrapper> tag 有三個 <app-photo> tags,AppPhotoComponent 組件被投影到<ng-content> 。 contentChildren 函式成功查詢 AppPhotoWrapperComponent 中的 AppPhotoComponent。
contentChildren 可以查詢元素、TemplateRef 和組件。第一個參數是一個 selector,它是範本變數 (template variables) 或類型。read 屬性指定 contentChildren 傳回的元素類型。contentChildren 函數傳回 signal 中的元素 array。 當函數找不到匹配項時,它會傳回一個 empty array,而不是錯誤。contentChildren 在 OnInit hook 後可用。鐵人賽的第 22 天就這樣結束了。